﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Collections.ObjectModel;
using System.IO;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;

namespace Tzeentch
{
    class Mosaic
    {
        public static Random rand;
        private const int _minimumPolygonCount = 80;
        private const int _maximumPolygonCount = 80;

        SolidBrush sb;
        Graphics g;

        #region Properties
        /// <summary>
        /// Gets or sets the collection of polygons.
        /// </summary>
        /// <value>
        /// The collection of polygons.
        /// </value>
        public List<Polygon> Polygons { get; set; } 

        /// <summary>
        /// Gets or sets the generated image.
        /// </summary>
        /// <value>
        /// The generated image.
        /// </value>
        public Bitmap GeneratedImage { get; set; }

        /// <summary>
        /// Gets or sets the original image. CONSIDER TURNING INTO A PRIVATE FIELD!
        /// </summary>
        /// <value>
        /// The original image.
        /// </value>
        public Bitmap OriginalImage { get; set; }

        /// <summary>
        /// Gets the value indicating how well generated image reflects the original image.
        /// </summary>
        public double Fitness { get; set; }

        #endregion

        #region Constructor
        /// <summary>
        /// Initializes a new instance of the <see cref="Mosaic"/> class.
        /// </summary>
        /// <param name="originalBitmap">The original bitmap.</param>
        public Mosaic(Bitmap originalBitmap)
        {            
            OriginalImage = originalBitmap;

            if (rand == null)
                rand = new Random();

            int numberOfPolygons = rand.Next(_minimumPolygonCount, _maximumPolygonCount);
            Polygons = new List<Polygon>();
            for (int i = 0 ; i < numberOfPolygons ; i++)
            {
                Polygons.Add(new Polygon(OriginalImage.Width, OriginalImage.Height,rand));
            }

            sb = new SolidBrush(Color.Black);
            GeneratedImage = new Bitmap(OriginalImage.Width, OriginalImage.Height);
            g = Graphics.FromImage(GeneratedImage);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Mosaic"/> class.
        /// </summary>
        /// <param name="mosaic">The mosaic.</param>
        public Mosaic(Mosaic mosaic)
        {
            OriginalImage = mosaic.OriginalImage;

            if (rand == null)
                rand = new Random();
            Polygons = new List<Polygon>();
            Polygons.Clear();
            foreach (var polygon in mosaic.Polygons)
            {
                Polygons.Add((Polygon)polygon.Clone());
            }

            sb = new SolidBrush(Color.Black);
            GeneratedImage = new Bitmap(OriginalImage.Width, OriginalImage.Height);
            g = Graphics.FromImage(GeneratedImage);
        }

        #endregion

        #region Methods
        /// <summary>
        /// Mutates this instance.
        /// </summary>
        public void Mutate()
        { 
            float chanceForColorMutation = 0.40F;
            float chanceForPointMove = 0.1F;
            float chanceForPolygonChange = 0.5F;
            float chanceForPolygonPositionChange = 0.1F;

            int minimalPointMove = 1;
            int maximalPointMove = 15;
            int minimalColorChange = -20;
            int maximalColorChange = 20;

            // Mutating points and color
            foreach (var polygon in Polygons)
            {
                if (rand.NextDouble() < chanceForColorMutation)
                    polygon.MutateChangeColor(minimalColorChange, maximalColorChange, rand);
                if (rand.NextDouble() < chanceForPointMove)
                    polygon.MutateMoveOneCorner(minimalPointMove, maximalPointMove, OriginalImage.Width, OriginalImage.Height, rand);
            }

            // Mutating position of polygon.
            if (rand.NextDouble() < chanceForPolygonPositionChange)
            {
                int polygonToReplace = rand.Next(Polygons.Count);
                Polygon temporaryPolygon = Polygons[polygonToReplace];
                Polygons.RemoveAt(polygonToReplace);
                Polygons.Add(temporaryPolygon);
            }

            // Creating new polygon instead of old one.
            if (rand.NextDouble() < chanceForPolygonChange)
            {
                Polygons.RemoveAt(rand.Next(Polygons.Count));
                Polygons.Add(new Polygon(OriginalImage.Width, OriginalImage.Height, rand));
            }
        }

        /// <summary>
        /// Renders the bitmap.
        /// </summary>
        public void RenderBitmap()
        {
            
            g.Clear(Color.FromArgb(0, 255, 255, 255));
            foreach (var item in Polygons)
            {
                sb.Color = item.Color;
                g.FillPolygon(sb, item.Corners);
            }            
        }

        /// <summary>
        /// Counts the fitness.
        /// </summary>
        public void CountFitness()
        {
            //Fitness = CompareImages();
            Fitness = UnsafeCompareImages();
        }

        double OneDividedSQRT3 = 1.0 / Math.Sqrt(3.0);
        float twohundredtwentyfifth = 1.0F / 255.0F;
        pixel[][] originalPixelMap = null;
        bool originalDataSet = false;
        byte[] origValues = null;

        /// <summary>
        /// Compares images.
        /// </summary>
        /// <returns></returns>
        public double UnsafeCompareImages()
        {
            float result = 0.0F;
            float val = 0.0F;
            int width = OriginalImage.Width;
            int height = OriginalImage.Height;

            
            Rectangle rect = new Rectangle(0, 0, OriginalImage.Width, OriginalImage.Height);

            if (!originalDataSet)
            {

                // Lock the bitmap's bits.  
                System.Drawing.Imaging.BitmapData originalData =
                    OriginalImage.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite,
                    OriginalImage.PixelFormat);

                // Get the address of the first line.
                IntPtr origPtr = originalData.Scan0;

                // Declare an array to hold the bytes of the bitmap.
                int origBytes = Math.Abs(originalData.Stride) * height;
                if (origValues == null)
                    origValues = new byte[origBytes];


                System.Runtime.InteropServices.Marshal.Copy(origPtr, origValues, 0, origBytes);

                OriginalImage.UnlockBits(originalData);

                originalDataSet = true;
            }

            // Lock the bitmap's bits.  
            System.Drawing.Imaging.BitmapData generatedData =
                GeneratedImage.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite,
                GeneratedImage.PixelFormat);

            // Get the address of the first line.
            IntPtr genPtr = generatedData.Scan0;

            // Declare an array to hold the bytes of the bitmap.
            int genBytes = Math.Abs(generatedData.Stride) * height;
            byte[] genValues = new byte[genBytes];

            System.Runtime.InteropServices.Marshal.Copy(genPtr, genValues, 0, genBytes);

            bool _24bpp = false;
            if (OriginalImage.PixelFormat == PixelFormat.Format24bppRgb)
                _24bpp = true;

            // i - iterator, o - iterator for original image, needs to be 3/4 of i in case of 24bpp image!
            for (int i = 0, o = 0; i < width * height * 4; i+=4)
            {

                if (_24bpp)
                    o = 3 * (i / 4);
                else
                    o = i;

                if (genValues[i+3] == 255)
                {
                    float diffR = (origValues[o + 2] - genValues[i + 2]) * twohundredtwentyfifth;
                    float diffG = (origValues[o + 1] - genValues[i + 1]) * twohundredtwentyfifth;
                    float diffB = (origValues[o] - genValues[i]) * twohundredtwentyfifth;

                    double r = (diffR * diffR);
                    double g = (diffG * diffG);
                    double b = (diffB * diffB);

                    val = (float)(Math.Sqrt(r + g + b) * OneDividedSQRT3);    // max (r + g + b) = 3
                    result += (1.0F - val);
                }
            }

            // Unlock the bits.            
            GeneratedImage.UnlockBits(generatedData);

            result /= (width * height);
            return (result);
        }

        /// <summary>
        /// Compares the original image with the generated image pixel by pixel and sums the color differences.
        /// </summary>
        /// <returns></returns>
        public double CompareImages()
        {
            float result = 0.0F;
            int fullPixels = 0;
            //float cover = 0.0F;
            pixel original, generated;
            //pixelLAB originalLAB, generatedLAB;
            //float diff = 0.0F;
            float val = 0.0F;
            int width = OriginalImage.Width;
            int height = OriginalImage.Height;

            if (originalPixelMap == null)
                originalPixelMap = ConvertToPixelArray(OriginalImage, width, height);
            pixel[][] generatedPixelMap = ConvertToPixelArray(GeneratedImage, width, height);

            for (int x = 0; x < width; x++)
                for (int y = 0; y < height; y++)
                {
                    //diff = 0.0F;
                    val = 0.0F;

                    original = originalPixelMap[x][y];
                    generated = generatedPixelMap[x][y];

                    if (generated.A == 255)
                    {
                        float diffR = (original.R - generated.R) * twohundredtwentyfifth;
                        float diffG = (original.G - generated.G) * twohundredtwentyfifth;
                        float diffB = (original.B - generated.B) * twohundredtwentyfifth;

                        double r = (diffR * diffR);
                        double g = (diffG * diffG);
                        double b = (diffB * diffB);

                        val = (float)(Math.Sqrt(r + g + b) * OneDividedSQRT3);    // max (r + g + b) = 3
                        result += (1.0F - val);
                    }

                    if (generatedPixelMap[x][y].A != 0)
                        fullPixels++; 
                }
            result /= (width * height);
            return (result);
        }

        /// <summary>
        /// Converts to pixel array.
        /// </summary>
        /// <param name="image">The image.</param>
        /// <param name="width">The width.</param>
        /// <param name="height">The height.</param>
        /// <returns>Array of pixels</returns>
        private pixel[][] ConvertToPixelArray(Bitmap image, int width, int height)
        {
            
            int step = 1;
            if (image.PixelFormat == PixelFormat.Format24bppRgb)
                step = 3;
            if (image.PixelFormat == PixelFormat.Format32bppArgb)
                step = 4;

            BitmapData bData = image.LockBits(new Rectangle(new Point(), image.Size),
            ImageLockMode.ReadOnly,
            image.PixelFormat);

            int byteCount = bData.Stride * height;
            byte[] inputBytes = new byte[byteCount];
            Marshal.Copy(bData.Scan0, inputBytes, 0, byteCount);
            image.UnlockBits(bData);

            pixel[][] output = new pixel[width][];
            for (int i = 0; i < width; i++)
            {
                output[i] = new pixel[height];
            }

            for (int i = 0; i < height; i++)
            {
                for (int j = 0; j < width; j++)
                {
                    int index = (i * width * step) + (j * step);
                    if(step ==3)
                    {
                        output[j][i].A = 0;
                        output[j][i].R = inputBytes[index];
                        output[j][i].G = inputBytes[index + 1];
                        output[j][i].B = inputBytes[index + 2];
                    }
                    else
                    {
                        output[j][i].A = inputBytes[index + 3];
                        output[j][i].R = inputBytes[index];
                        output[j][i].G = inputBytes[index + 1];
                        output[j][i].B = inputBytes[index + 2];
                    }
                }
            }

            return output;
        }

        /// <summary>
        /// RGs the bto LAB.
        /// </summary>
        /// <param name="input">The input.</param>
        /// <returns>Pixel struct with LAB format</returns>
        public pixelLAB RGBtoLAB(pixel input)
        {
            pixelLAB output;
            // normalize red, green, blue values
              double rLinear = (double)input.R/255.0;
              double gLinear = (double)input.G/255.0;
              double bLinear = (double)input.B/255.0;

              double x, y, z;
  
              // convert to a sRGB form
              double r = (rLinear > 0.04045)? Math.Pow((rLinear + 0.055)/(1 + 0.055), 2.2) : (rLinear/12.92) ;
              double g = (gLinear > 0.04045)? Math.Pow((gLinear + 0.055)/(1 + 0.055), 2.2) : (gLinear/12.92) ;
              double b = (bLinear > 0.04045)? Math.Pow((bLinear + 0.055)/(1 + 0.055), 2.2) : (bLinear/12.92) ;
  
              // converts
              x = r * 0.4124 + g * 0.3576 + b * 0.1805;
              y = r * 0.2126 + g * 0.7152 + b * 0.0722;
              z = r * 0.0193 + g * 0.1192 + b * 0.9505;

              x = (x > 0.9505) ? 0.9505 : ((x < 0) ? 0 : x);
              y = (y > 1.0) ? 1.0 : ((y < 0) ? 0 : y);
              z = (z > 1.089) ? 1.089 : ((z < 0) ? 0 : z);

              output.L = 116.0 * Fxyz(y / 1.0) - 16;
              output.A = 500.0 * (Fxyz(x / 0.9505) - Fxyz(y / 1.0));
              output.B = 200.0 * (Fxyz(y / 1.0) - Fxyz(z / 1.0890));
              output.Alpha = input.A;
              return output;
        }

        private static double Fxyz(double t)
         {
             return ((t > 0.008856)? Math.Pow(t, (1.0/3.0)) : (7.787*t + 16.0/116.0));
         }

        internal struct pixel
        {
            public pixel(int A, int R, int G, int B)
            {
                this.A=A;
                this.B=B;
                this.R=R;
                this.G=G;
            }
            public int A;
            public int R;
            public int G;
            public int B;
        }

        internal struct pixelLAB
        {
            public double L;
            public double A;
            public double B;
            public int Alpha;
        }
        #endregion
    }
}
